<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <title>canvas文本格式屏幕录制</title>
    <style>
      .container {
        width: 100%;
        display: flex;
        flex-flow: row nowrap;
        justify-content: space-between;
      }
      .paint-area {
        flex: 0 1 45%;
        padding-right: 2%;
        border-right: 1px solid #eee;
      }
      .video-area {
        flex: 0 1 45%;
      }
      .canvas-box {
        border: 1px solid #eee;
        margin: 0 auto;
        display: block;
        position: relative;
      }
      .canvas-area {
        position: relative;
      }
      .canvas-area canvas {
        position: absolute;
        z-index: 1;
      }
      .canvas-image {
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        background-repeat: no-repeat;
        background-size: 100%;
        pointer-events: none;
      }
      .time {
        width: 100%;
        height: 20px;
        text-align: center;
        background-color: rgba(144, 144, 144, 0.1);
      }
      video {
        border: 1px solid #eee;
        display: none;
      }
      textarea{
        display: block;
      }
      .margin {
        margin: 10px 0;
      }
    </style>
</head>
<body>
 
  <div class='container'>
    <div class="paint-area">
      <p>画板区</p>
      <div class="canvas-box">
        <div class="time" id="time"></div>
        <div class="canvas-area">
          <canvas id='canvas-record'></canvas>
          <div class="canvas-image" id="canvas-image"></div>
        </div>
      </div>
      <div class="margin">
        <button id="startRecord" onclick='startRecord()'>开始录制</button>
        <button id="pauseRecord" onclick='pauseRecord()'>暂停录制</button>
        <button id="resumeRecord" onclick='resumeRecord()'>继续录制</button>
        <button id="stopRecord" onclick='stopRecord()'>结束录制</button>
      </div>
      <div class="margin">
        <button id="clearAll" onclick='clearAll()'>清除绘制</button>
        <button id="insertImage" onclick="insertImage()">插入背景图片</button>
        <p>输出数据：</p>
        <textarea id="canvas-code" cols="100" rows="6" class="margin"></textarea>
      </div>
    </div>
    
    <div class="video-area">
      <p>视频区</p>
      <video id='video-record' playsInline autoPlay>
      </video>
      <div>
        <p>输入数据：</p>
        <textarea id="video-code" cols="100" rows="6" class="margin"></textarea>
        <button id="startPlay" onclick='startPlay()'>开始播放</button>
        <button id="exportVideo" onclick='exportVideo()'>导出视频</button>
      </div>
    </div>
    
  </div>

<script>
let canvas, context, video, stream = null
let recordeBlobs = [] // 二进制视频文件
let mediaRecorder = null // 媒体对象
let canvasHistory = [] // 历史栈
let time = 0, timer // 录制时间定时器
let timeDom = null // 时间展示dom
let lastTime = 0 // 上一次记录的时间
/**
 * 初始化canvas
 **/
const initCanvas = () => {
  const canvasArea = document.querySelector('.canvas-area')
  console.log(canvasArea);
  canvasArea.style.width = 800 + 'px'
  canvasArea.style.height = 400 + 'px'
  canvas = document.querySelector('#canvas-record');
  // 设置canvas宽高
  canvas.width = 800
  canvas.height = 400
  
  context = canvas.getContext('2d');
  // 下面两行必须要，不然录制视频里的背景为默认黑色，此处为修改canvas背景色
  // context.fillStyle = '#fff'
  // context.fillRect(0, 0, canvas.width, canvas.height)
  // 笔触样式
  context.lineWidth = 1;

  canvas.addEventListener('mousedown', startAction);
  canvas.addEventListener('mouseup', endAction);
};
/**
 * 初始化时间
 **/
const initTime = () => {
  timeDom = document.querySelector('#time')
  timeDom.textContent = format(time)
}
/**
 * 允许绘制
 **/
const canDraw = () => {
  canvas.addEventListener('mousedown', startAction);
  canvas.addEventListener('mouseup', endAction);
}
/**
 * canvas涂鸦事件开始
 **/
const startAction = (e) => {
  context.strokeStyle = '#000';
  context.beginPath();
  context.moveTo(e.offsetX, e.offsetY);
  context.stroke();
  canvasHistory.push([{
    f: 'pen',
    s: 'start',
    x: e.offsetX,
    y: e.offsetY,
    t: autoFixed(time - lastTime, 3)
  }])
  lastTime = time
  canvas.addEventListener('mousemove', moveAction);
};
/**
 * canvas涂鸦事件过程
 **/
const moveAction = (e) => {
  context.lineTo(e.offsetX, e.offsetY);
  context.stroke();
  canvasHistory[canvasHistory.length - 1].push({
    f: 'pen',
    s: 'ing',
    x: e.offsetX,
    y: e.offsetY,
    t: autoFixed(time - lastTime, 3)
  })
  lastTime = time
};
/**
 * canvas涂鸦事件结束
 **/
const endAction = (e) => {
  context.closePath();
  canvasHistory[canvasHistory.length - 1].push({
    f: 'pen',
    s: 'end',
    x: e.offsetX,
    y: e.offsetY,
    t: autoFixed(time - lastTime, 3)
  })
  lastTime = time
  document.querySelector('#canvas-code').value = JSON.stringify(canvasHistory)
  canvas.removeEventListener('mousemove', moveAction);
};
/**
 * 开始播放
 **/
const startPlay = async (e) => {
  const isSupported = isSupportedRecord()
  if (!isSupported) return
  video = document.querySelector('#video-record');
  video.width = 400;
  video.height = 200;
  video.style.display = 'block'
  const can = document.createElement('canvas')
  const ctx = can.getContext('2d')
  can.width = 800
  can.height = 400
  ctx.fillStyle = '#fff'
  ctx.fillRect(0, 0, can.width, can.height)
  
  stream = can.captureStream(25);
  video.srcObject = stream; // 屏幕实时回显
  recordVideo(stream)
  const videoCode = document.querySelector('#video-code').value
  for (const item of JSON.parse(videoCode)) {
    await new Promise((resolve, reject) => {
      const flag = singleAnnoyPathDraw(can, item)
      if (flag) {
        resolve(flag)
      }
    })
  }
};
/**
 * 视频录制中
 **/
const recordVideo = (stream) => {
  recordeBlobs = [];
  if (!mediaRecorder) {
    mediaRecorder = new MediaRecorder(stream, {
      mimeType: 'video/webm;codecs=vp8',
    });
    mediaRecorder.onstop = (e) => {
      console.log('录制完成');
    };
    mediaRecorder.ondataavailable = (e) => {
      console.log(e.data);
      if (e.data && e.data.size > 0) {
        recordeBlobs.push(e.data);
      }
    };
    mediaRecorder.start(100);
  }
}
/**
 * 导出视频文件
 **/
const exportVideo = () => {
  if (!mediaRecorder) return
  mediaRecorder.stop();
  // stream.getTracks().forEach((track) => track.stop());
  // stream = null;
  if (recordeBlobs.length === 0) return
  const blob = new Blob(recordeBlobs, { type: 'video/webm;codecs=vp8' });
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.style.display = 'none';
  a.href = url;
  a.download = '屏幕录制.webm';
  document.body.appendChild(a);
  a.click();
  setTimeout(() => {
    document.body.removeChild(a);
    window.URL.revokeObjectURL(url);
  }, 100);
}
/**
 * 单次记录绘制
 * @param curStack 单次历史栈中的记录
 */
const singleAnnoyPathDraw = async (can, curStack) => {
  const ctx = can.getContext('2d')
  // 图片插入
  if (curStack.f === 'image') {
    await new Promise((resolve, reject) => {
      setTimeout(() => {
        drawImageToCanvas(can, curStack.s)
        resolve(true)
      }, curStack.t * 1000)
    })
  // 涂鸦绘制
  } else {
    for (const item of curStack) {
      await new Promise((resolve, reject) => {
        setTimeout(() => {
          if (item.s === 'start') {
            ctx.beginPath()
            ctx.moveTo(item.x, item.y)
            ctx.stroke()
          } else if (item.s === 'ing') {
            ctx.lineTo(item.x, item.y)
            ctx.stroke()
          } else {
            ctx.closePath()
          }
          resolve(true)
        }, item.t * 1000)
      })
    }
  }
  
}
/**
 * 绘制图片到canvas上(视频区)
 **/
 const drawImageToCanvas = (can, imgData) => {
  // new Promise((resolve, reject) => {
  //   const ctx = can.getContext('2d')
  //   const img = new Image;
  //   img.src = imgData;
  //   const annoyCanvas = document.createElement('canvas')
  //   const annoyContext = annoyCanvas.getContext('2d')
  //   annoyCanvas.width = 800
  //   annoyCanvas.height = 400 
  //   img.onload = () => {
  //     annoyContext.drawImage(img, 0, 0, img.width, img.height, 0, 0, annoyCanvas.width, annoyCanvas.height)
  //     resolve(annoyCanvas)
  //   }
  // }).then(res => {
  //   setBgImageToCanvas(res, can)
  // })
  const ctx = can.getContext('2d')
    const img = new Image;
    img.src = imgData;
    img.onload = () => {
      ctx.drawImage(img, 0, 0, can.width, can.height)
    }
}
/**
 * 设置canvas背景图
 * @param {*} img
 * @param {*} can
 */
const setBgImageToCanvas = (img, can) => {
  const ctx = can.getContext('2d')
  const bg = ctx.createPattern(img, "no-repeat"); // createPattern() 方法在指定的方向内重复指定的元素。
  ctx.fillStyle = bg; // fillStyle 属性设置或返回用于填充绘画的颜色、渐变或模式。
  ctx.fillRect(0, 0, can.width, can.height); // 绘制已填充矩形fillRect(左上角x坐标, 左上角y坐标, 宽, 高)
}
/**
 * 开始录制
 **/
const startRecord = (stream) => {
  timer = setInterval(() => {
    time += 0.025
    timeDom.textContent = format(time)
  }, 25)
}
/**
 * 暂停录制
 **/
const pauseRecord = () => {
  clearInterval(timer)
  timer = null
}
/**
 * 继续录制
 **/
const resumeRecord = () => {
  if (!timer) {
    timer = setInterval(() => {
      time += 0.025
      timeDom.textContent = format(time)
    }, 25)
  }
}
/**
 * 结束录制
 **/
const stopRecord = () => {
  timer && clearInterval(timer)
  time = 0
  // btnStatus('stopRecord', true)
};
/**
 * 按钮禁用/解除禁用方法
 * @param id 按钮id
 * @param status 是否禁用
 */
const btnStatus = (id, status) => {
  const button = document.querySelector(`#${id}`)
  button.disabled = status
}
/**
 * 时间格式化
 * @param {*} s
 */
const format = (s) => {
  const minute = parseInt(s / 60 % 60)
  const hour = parseInt(s / 3600) < 10 ? '0' + parseInt(s / 3600) : parseInt(s / 3600)
  const second = Math.ceil(s % 60)  > 59 ? 59 : Math.ceil(s % 60) 
  return `${hour > 0 ? `${hour}:` : ''}${minute < 10 ? '0' + minute : minute}:${second < 10 ? '0' + second : second}`
}
/**
 * 判断其 MIME 格式能否被客户端录制
 **/
const isSupportedRecord = () => {
  const mimeType = 'video/webm;codecs=vp8'
  return MediaRecorder.isTypeSupported(mimeType)
}
/**
 * 清除全部
 **/
const clearAll = () => {
  canvasHistory = [] // 清除涂鸦历史栈
  context && context.clearRect(0, 0, canvas.width, canvas.height) // 清除绘制区域
}
/**
 * 手动实现toFixed功能
 * @param value 值
 * @param n 保留几位小数
 */
const autoFixed = (value, n) => {
  return Math.round(value * Math.pow(10, n)) / Math.pow(10, n);
}
/**
 * 插入本地背景图片
 **/
const insertImage = (id) => {
  const inputObj = document.createElement('input');
  inputObj.addEventListener('change', readFile, false);
  inputObj.type = 'file';
  inputObj.accept = 'image/*';
  inputObj.id = id;
  inputObj.click();
  pauseRecord()
}
/**
 * 解析本地图片并转化为base64
 **/
const readFile = function() {
  const file = this.files[0]; // 获取input输入的图片
  if(!/image\/\w+/.test(file.type)){
      alert("请确保文件为图像类型");
      return false;
  } // 判断是否图片
  const reader = new FileReader();
  reader.readAsDataURL(file); // 转化成base64数据类型
  reader.onload = function(e){
    drawLocalImageToCanvas(this.result);
  }
}
/**
 * 绘制本地图片到canvas上（画板区）
 **/
const drawLocalImageToCanvas = (imgData) => {
  const canvasImage = document.querySelector('#canvas-image')
  canvasImage.style.backgroundImage = `url(${imgData})`
  canvasHistory.push({
    f: 'image',
    s: imgData,
    t: autoFixed(time - lastTime, 3)
  })
  lastTime = time
  document.querySelector('#canvas-code').value = JSON.stringify(canvasHistory)
  resumeRecord()
}
initCanvas()
initTime()
</script>  

</body>
</html>
